/*
 * (C) 2000-2006 Stuart Caie <kyzer@4u.net>
 * (C) 2007-2008 Ben Motmans <ben.motmans@gmail.com> (modifications for native interop with mono)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/* cabextract uses libmspack to access cabinet files. libmspack is
 * available from http://www.kyz.uklinux.net/libmspack/
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdbool.h>

#include <libgalaxium.h>
#include <mspack/mspack.h>

struct mscab_decompressor *cabd = NULL;

/* prototypes */
static void load_spanning_cabinets(struct mscabd_cabinet *basecab, char *filename);
static char *find_cabinet_file(char *origcab, char *cabname);
static int unix_path_seperators(struct mscabd_file *files);
static char *create_output_name(unsigned char *fname, unsigned char *dirq);

static struct mspack_file *cabx_open(struct mspack_system *this, char *filename, int mode);

static void cabx_close(struct mspack_file *file);
static int cabx_read(struct mspack_file *file, void *buffer, int bytes);
static int cabx_write(struct mspack_file *file, void *buffer, int bytes);
static int cabx_seek(struct mspack_file *file, off_t offset, int mode);
static off_t cabx_tell(struct mspack_file *file);
static void cabx_msg(struct mspack_file *file, char *format, ...);
static void *cabx_alloc(struct mspack_system *this, size_t bytes);
static void cabx_free(void *buffer);
static void cabx_copy(void *src, void *dest, size_t bytes);

struct mspack_file_p
{
	FILE *fh;
	char *name, regular_file;
};

static struct mspack_system cabextract_system =
{
	&cabx_open, &cabx_close, &cabx_read,  &cabx_write, &cabx_seek,
	&cabx_tell, &cabx_msg, &cabx_alloc, &cabx_free, &cabx_copy, NULL
};

bool libgalaxium_cab_init ()
{
	int err;
	MSPACK_SYS_SELFTEST(err);
	if (err) {
		fprintf(stderr, "selftest error %d\n", err);
		return false;
	}

	if (!(cabd = mspack_create_cab_decompressor(&cabextract_system))) {
		fprintf(stderr, "can't create libmspack CAB decompressor\n");
		return false;
	}

	return true;
}

bool libgalaxium_cab_extract (char* filename, char* output_dir)
{
	struct mscabd_cabinet *basecab, *cab, *cab2;
	struct mscabd_file *file;
	int isunix, errors = 0;
	char *name;

	/* search the file for cabinets */
	if (!(basecab = cabd->search(cabd, filename))) {
		return false;
	}

	/* iterate over all cabinets found in that file */
	for (cab = basecab; cab; cab = cab->next) {
		/* load all spanning cabinets */
		load_spanning_cabinets(cab, filename);

		/* determine whether UNIX or MS-DOS path seperators are used */
		isunix = unix_path_seperators(cab->files);

		/* process all files */
		for (file = cab->files; file; file = file->next) {
			/* create the full UNIX output filename */
			if (!(name = create_output_name((unsigned char *)file->filename, (unsigned char *)output_dir))) {
				errors++;
				continue;
			}

		  	printf("  extracting %s\n", name);

			if (cabd->extract(cabd, file, name)) {
				errors++;
			}

			free(name);
		}

		/* free the spanning cabinet filenames [not freed by cabd->close()] */
		for (cab2 = cab->prevcab; cab2; cab2 = cab2->prevcab) free(cab2->filename);
		for (cab2 = cab->nextcab; cab2; cab2 = cab2->nextcab) free(cab2->filename);
	}

	/* free all loaded cabinets */
	cabd->close(cabd, basecab);
	if (errors > 0)
		return false;
	else
		return true;
}

void libgalaxium_cab_destroy ()
{
	mspack_destroy_cab_decompressor(cabd);
}

static char *create_output_name (unsigned char* filename, unsigned char* output_dir)
{
	unsigned int len = strlen ((char *)filename) + strlen ((char *)output_dir) + 1;

	unsigned char* fullname = malloc(len);

	*fullname = '\0';
	strcpy((char *)fullname, (char *)output_dir);
	strcat((char *)fullname, "/");
	strcat((char *)fullname, (char *)filename);

	return (char *)fullname;
}

static int unix_path_seperators(struct mscabd_file *files)
{
	struct mscabd_file *fi;
	char slash=0, backslash=0, *oldname;
	int oldlen;

	for (fi = files; fi; fi = fi->next) {
		char *p;
		for (p = fi->filename; *p; p++) {
			if (*p == '/') slash = 1;
			if (*p == '\\') backslash = 1;
		}
		if (slash && backslash) break;
	}

	if (slash) {
		/* slashes, but no backslashes = UNIX */
		if (!backslash) return 1;
	} else {
		/* no slashes = MS-DOS */
		return 0;
	}

	/* special case if there's only one file - just take the first slash */
	if (!files->next) {
		char c, *p = fi->filename;
		while ((c = *p++)) {
			if (c == '\\') return 0; /* backslash = MS-DOS */
			if (c == '/')  return 1; /* slash = UNIX */
		}
		/* should not happen - at least one slash was found! */
		return 0;
	}

	oldname = NULL;
	oldlen = 0;
	for (fi = files; fi; fi = fi->next) {
		char *name = fi->filename;
		int len = 0;
		while (name[len]) {
			if ((name[len] == '\\') || (name[len] == '/')) break;
			len++;
		}
		if (!name[len]) len = 0; else len++;

		if (len && (len == oldlen)) {
			if (strncmp(name, oldname, (size_t) len) == 0)
			return (name[len-1] == '\\') ? 0 : 1;
		}
		oldname = name;
		oldlen = len;
	}

	return 0;
}

static void load_spanning_cabinets(struct mscabd_cabinet *basecab, char *filename)
{
	struct mscabd_cabinet *cab, *cab2;
	char *name;

	/* load any spanning cabinets -- backwards */
	for (cab = basecab; cab->flags & MSCAB_HDR_PREVCAB; cab = cab->prevcab) {
		if (!(name = find_cabinet_file(filename, cab->prevname))) {
			fprintf(stderr, "%s: can't find %s\n", filename, cab->prevname);
			break;
		}
		if (!(cab2 = cabd->open(cabd,name)) || cabd->prepend(cabd, cab, cab2)) {
			if (cab2) cabd->close(cabd, cab2);
			break;
		}
	}

	/* load any spanning cabinets -- forwards */
	for (cab = basecab; cab->flags & MSCAB_HDR_NEXTCAB; cab = cab->nextcab) {
		if (!(name = find_cabinet_file(filename, cab->nextname))) {
			fprintf(stderr, "%s: can't find %s\n", filename, cab->nextname);
			break;
		}
		if (!(cab2 = cabd->open(cabd,name)) || cabd->append(cabd, cab, cab2)) {
			if (cab2) cabd->close(cabd, cab2);
			break;
		}
	}
}

static char *find_cabinet_file(char *origcab, char *cabname)
{
	return origcab;
}

static struct mspack_file *cabx_open(struct mspack_system *this, char *filename, int mode)
{
	struct mspack_file_p *fh;
	char *fmode;

	/* ensure that mode is one of READ, WRITE, UPDATE or APPEND */
	switch (mode) {
		case MSPACK_SYS_OPEN_READ:   fmode = "rb";  break;
		case MSPACK_SYS_OPEN_WRITE:  fmode = "wb";  break;
		case MSPACK_SYS_OPEN_UPDATE: fmode = "r+b"; break;
		case MSPACK_SYS_OPEN_APPEND: fmode = "ab";  break;
		default: return NULL;
	}

	if ((fh = malloc(sizeof(struct mspack_file_p)))) {
		fh->name = filename;

		/* regular file - simply attempt to open it */
		fh->regular_file = 1;
		if ((fh->fh = fopen(filename, fmode))) {
			return (struct mspack_file *) fh;
		}

		/* error - free file handle and return NULL */
		free(fh);
	}
	return NULL;
}

static void cabx_close(struct mspack_file *file)
{
	struct mspack_file_p *this = (struct mspack_file_p *) file;
	if (this) {
		if (this->regular_file) {
			fclose(this->fh);
		}
		free(this);
	}
}

static int cabx_read(struct mspack_file *file, void *buffer, int bytes)
{
	struct mspack_file_p *this = (struct mspack_file_p *) file;
	if (this && this->regular_file) {
		size_t count = fread(buffer, 1, (size_t) bytes, this->fh);
		if (!ferror(this->fh)) return (int) count;
	}
	return -1;
}

static int cabx_write(struct mspack_file *file, void *buffer, int bytes)
{
	struct mspack_file_p *this = (struct mspack_file_p *) file;
	if (this) {
		/* regular files and the stdout writer */
		size_t count = fwrite(buffer, 1, (size_t) bytes, this->fh);
		if (!ferror(this->fh)) return (int) count;
	}
	return -1;
}

static int cabx_seek(struct mspack_file *file, off_t offset, int mode)
{
	struct mspack_file_p *this = (struct mspack_file_p *) file;
	if (this && this->regular_file) {
		switch (mode) {
			case MSPACK_SYS_SEEK_START: mode = SEEK_SET; break;
			case MSPACK_SYS_SEEK_CUR:   mode = SEEK_CUR; break;
			case MSPACK_SYS_SEEK_END:   mode = SEEK_END; break;
			default: return -1;
		}
		#if HAVE_FSEEKO
			return fseeko(this->fh, offset, mode);
		#else
			return fseek(this->fh, offset, mode);
		#endif
	}
	return -1;
}

static off_t cabx_tell(struct mspack_file *file)
{
	struct mspack_file_p *this = (struct mspack_file_p *) file;
	#if HAVE_FSEEKO
		return (this && this->regular_file) ? (off_t) ftello(this->fh) : 0;
	#else
		return (this && this->regular_file) ? (off_t) ftell(this->fh) : 0;
	#endif
}

static void cabx_msg(struct mspack_file *file, char *format, ...)
{
  va_list ap;
  if (file) {
    fprintf(stderr, "%s: ", ((struct mspack_file_p *) file)->name);
  }
  va_start(ap, format);
  vfprintf(stderr, format, ap);
  va_end(ap);
  fputc((int) '\n', stderr);
  fflush(stderr);
}

static void *cabx_alloc(struct mspack_system *this, size_t bytes)
{
	return malloc(bytes);
}

static void cabx_free(void *buffer)
{
  free(buffer);
}

static void cabx_copy(void *src, void *dest, size_t bytes)
{
	memcpy(dest, src, bytes);
}
